MaĂźtrisez l'optimisation des performances WebGL avec notre guide approfondi sur les requĂȘtes de pipeline. Apprenez Ă mesurer le temps GPU et Ă identifier les goulots d'Ă©tranglement.
LibĂ©rer la performance du GPU : Un guide complet sur les requĂȘtes de pipeline WebGL
Dans le monde des graphismes web, la performance n'est pas qu'une simple fonctionnalitĂ© ; c'est le fondement d'une expĂ©rience utilisateur captivante. Un 60 images par seconde (IPS) fluide comme de la soie peut faire la diffĂ©rence entre une application 3D immersive et un dĂ©sastre frustrant et saccadĂ©. Alors que les dĂ©veloppeurs se concentrent souvent sur l'optimisation du code JavaScript, une bataille de performance critique se dĂ©roule sur un autre front : l'unitĂ© de traitement graphique (GPU). Mais comment optimiser ce que l'on ne peut pas mesurer ? C'est lĂ que les requĂȘtes de pipeline WebGL entrent en jeu.
Traditionnellement, mesurer la charge de travail du GPU cĂŽtĂ© client a Ă©tĂ© une boĂźte noire. Les minuteurs JavaScript standards comme performance.now() peuvent vous dire combien de temps le CPU a mis pour soumettre les commandes de rendu, mais ils ne rĂ©vĂšlent rien sur le temps que le GPU a mis pour les exĂ©cuter rĂ©ellement. Ce guide propose une analyse approfondie de l'API WebGL Query, un ensemble d'outils puissants qui vous permet de jeter un Ćil Ă l'intĂ©rieur de cette boĂźte noire, de mesurer des mĂ©triques spĂ©cifiques au GPU et de prendre des dĂ©cisions basĂ©es sur des donnĂ©es pour optimiser votre pipeline de rendu.
Qu'est-ce qu'un pipeline de rendu ? Un bref rappel
Avant de pouvoir mesurer le pipeline, nous devons comprendre ce que c'est. Un pipeline graphique moderne est une série d'étapes programmables et à fonction fixe qui transforment les données de votre modÚle 3D (sommets, textures) en pixels 2D que vous voyez à l'écran. En WebGL, cela inclut généralement :
- Shader de sommet (Vertex Shader) : Traite les sommets individuels, les transformant dans l'espace de découpage (clip space).
- Rastérisation : Convertit les primitives géométriques (triangles, lignes) en fragments (pixels potentiels).
- Shader de fragment (Fragment Shader) : Calcule la couleur finale pour chaque fragment.
- Opérations par fragment : Des tests comme les vérifications de profondeur et de stencil sont effectués, et la couleur finale du fragment est mélangée dans le tampon d'image (framebuffer).
Le concept crucial Ă saisir est la nature asynchrone de ce processus. Le CPU, qui exĂ©cute votre code JavaScript, agit comme un gĂ©nĂ©rateur de commandes. Il empaquette les donnĂ©es et les appels de dessin et les envoie au GPU. Le GPU traite ensuite ce tampon de commandes selon son propre calendrier. Il y a un dĂ©lai significatif entre l'appel CPU Ă gl.drawArrays() et le moment oĂč le GPU termine rĂ©ellement le rendu de ces triangles. Cet Ă©cart CPU-GPU est la raison pour laquelle les minuteurs CPU sont trompeurs pour l'analyse des performances GPU.
Le problĂšme : Mesurer l'invisible
Imaginez que vous essayez d'identifier la partie la plus gourmande en performance de votre scÚne. Vous avez un personnage complexe, un environnement détaillé et un effet de post-traitement sophistiqué. Vous pourriez essayer de chronométrer chaque partie en JavaScript :
const t0 = performance.now();
renderCharacter();
const t1 = performance.now();
renderEnvironment();
const t2 = performance.now();
renderPostProcessing();
const t3 = performance.now();
console.log(`Temps CPU personnage : ${t1 - t0}ms`); // Trompeur !
console.log(`Temps CPU environnement : ${t2 - t1}ms`); // Trompeur !
console.log(`Temps CPU post-traitement : ${t3 - t2}ms`); // Trompeur !
Les temps que vous obtiendrez seront incroyablement faibles et presque identiques. C'est parce que ces fonctions ne font que mettre en file d'attente des commandes. Le vrai travail se fait plus tard sur le GPU. Vous n'avez aucune idĂ©e si ce sont les shaders complexes du personnage ou la passe de post-traitement qui constituent le vĂ©ritable goulot d'Ă©tranglement. Pour rĂ©soudre cela, nous avons besoin d'un mĂ©canisme qui demande des donnĂ©es de performance au GPU lui-mĂȘme.
Introduction aux requĂȘtes de pipeline WebGL : Votre boĂźte Ă outils de performance GPU
Les objets de requĂȘte WebGL (WebGL Query Objects) sont la solution. Ce sont des objets lĂ©gers que vous pouvez utiliser pour poser des questions spĂ©cifiques au GPU sur le travail qu'il effectue. Le flux de travail principal consiste Ă placer des "marqueurs" dans le flux de commandes du GPU, puis Ă demander ultĂ©rieurement le rĂ©sultat de la mesure entre ces marqueurs.
Cela vous permet de poser des questions comme :
- "Combien de nanosecondes a-t-il fallu pour rendre la carte d'ombres (shadow map) ?"
- "Est-ce que des pixels du monstre caché derriÚre le mur étaient réellement visibles ?"
- "Combien de particules ma simulation GPU a-t-elle réellement générées ?"
En rĂ©pondant Ă ces questions, vous pouvez identifier prĂ©cisĂ©ment les goulots d'Ă©tranglement, mettre en Ćuvre des techniques d'optimisation avancĂ©es comme l'Ă©limination de l'occlusion (occlusion culling) et crĂ©er des applications dynamiquement Ă©volutives qui s'adaptent au matĂ©riel de l'utilisateur.
Bien que certaines requĂȘtes fussent disponibles en tant qu'extensions dans WebGL1, elles font partie intĂ©grante et standardisĂ©e de l'API WebGL2, qui est notre centre d'intĂ©rĂȘt dans ce guide. Si vous commencez un nouveau projet, cibler WebGL2 est fortement recommandĂ© pour son riche ensemble de fonctionnalitĂ©s et son large support par les navigateurs.
Types de requĂȘtes de pipeline en WebGL2
WebGL2 offre plusieurs types de requĂȘtes, chacune conçue pour un but spĂ©cifique. Nous explorerons les trois plus importantes.
1. RequĂȘtes de temps (`TIME_ELAPSED`) : Le chronomĂštre pour votre GPU
C'est sans doute la requĂȘte la plus prĂ©cieuse pour le profilage gĂ©nĂ©ral des performances. Elle mesure le temps d'horloge murale, en nanosecondes, que le GPU passe Ă exĂ©cuter un bloc de commandes.
Objectif : Mesurer la durée de passes de rendu spécifiques. C'est votre principal outil pour découvrir quelles parties de votre image sont les plus coûteuses.
Utilisation de l'API :
gl.createQuery(): CrĂ©e un nouvel objet de requĂȘte.gl.beginQuery(target, query): DĂ©marre la mesure. Pour les requĂȘtes de temps, la cible estgl.TIME_ELAPSED.gl.endQuery(target): ArrĂȘte la mesure.gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE): Demande si le rĂ©sultat est prĂȘt (renvoie un boolĂ©en). Ceci est non bloquant.gl.getQueryParameter(query, gl.QUERY_RESULT): Obtient le rĂ©sultat final (un entier en nanosecondes). Attention : Cela peut bloquer le pipeline si le rĂ©sultat n'est pas encore disponible.
Exemple : Profilage d'une passe de rendu
Ăcrivons un exemple pratique sur la façon de chronomĂ©trer une passe de post-traitement. Un principe clĂ© est de ne jamais bloquer en attendant un rĂ©sultat. Le modĂšle correct est de commencer la requĂȘte dans une image et de vĂ©rifier le rĂ©sultat dans une image ultĂ©rieure.
// --- Initialisation (exécutée une fois) ---
const gl = canvas.getContext('webgl2');
const postProcessingQuery = gl.createQuery();
let lastQueryResult = 0;
let isQueryInProgress = false;
// --- Boucle de rendu (exécutée à chaque image) ---
function render() {
// 1. VĂ©rifier si une requĂȘte d'une image prĂ©cĂ©dente est prĂȘte
if (isQueryInProgress) {
const available = gl.getQueryParameter(postProcessingQuery, gl.QUERY_RESULT_AVAILABLE);
const disjoint = gl.getParameter(gl.GPU_DISJOINT_EXT); // Vérifier les événements disjoints
if (available && !disjoint) {
// Le rĂ©sultat est prĂȘt et valide, rĂ©cupĂ©rez-le !
const timeElapsed = gl.getQueryParameter(postProcessingQuery, gl.QUERY_RESULT);
lastQueryResult = timeElapsed / 1_000_000; // Convertir les nanosecondes en millisecondes
isQueryInProgress = false;
}
}
// 2. Rendre la scĂšne principale...
renderScene();
// 3. DĂ©marrer une nouvelle requĂȘte si aucune n'est dĂ©jĂ en cours
if (!isQueryInProgress) {
gl.beginQuery(gl.TIME_ELAPSED, postProcessingQuery);
// Ămettre les commandes que nous voulons mesurer
renderPostProcessingPass();
gl.endQuery(gl.TIME_ELAPSED);
isQueryInProgress = true;
}
// 4. Afficher le rĂ©sultat de la derniĂšre requĂȘte terminĂ©e
updateDebugUI(`Temps GPU Post-Traitement : ${lastQueryResult.toFixed(2)} ms`);
requestAnimationFrame(render);
}
Dans cet exemple, nous utilisons le drapeau isQueryInProgress pour nous assurer de ne pas dĂ©marrer une nouvelle requĂȘte avant que le rĂ©sultat de la prĂ©cĂ©dente n'ait Ă©tĂ© lu. Nous vĂ©rifions Ă©galement GPU_DISJOINT_EXT. Un Ă©vĂ©nement "disjoint" (comme le systĂšme d'exploitation changeant de tĂąche ou le GPU modifiant sa vitesse d'horloge) peut invalider les rĂ©sultats du minuteur, il est donc de bonne pratique de le vĂ©rifier.
2. RequĂȘtes d'occlusion (`ANY_SAMPLES_PASSED`) : Le test de visibilitĂ©
L'Ă©limination de l'occlusion (occlusion culling) est une technique d'optimisation puissante oĂč vous Ă©vitez de rendre des objets qui sont complĂštement cachĂ©s (occlus) par d'autres objets plus proches de la camĂ©ra. Les requĂȘtes d'occlusion sont l'outil accĂ©lĂ©rĂ© par le matĂ©riel pour ce travail.
Objectif : Déterminer si un fragment d'un appel de dessin (ou d'un groupe d'appels) passerait le test de profondeur et serait visible à l'écran. Il ne compte pas combien de fragments ont réussi le test, seulement si le compte est supérieur à zéro.
Utilisation de l'API : L'API est la mĂȘme, mais la cible est gl.ANY_SAMPLES_PASSED.
Cas d'utilisation pratique : L'élimination de l'occlusion
La stratĂ©gie consiste Ă d'abord rendre une reprĂ©sentation simple et Ă faible nombre de polygones d'un objet (comme sa boĂźte englobante). Nous enveloppons cet appel de dessin peu coĂ»teux dans une requĂȘte d'occlusion. Dans une image ultĂ©rieure, nous vĂ©rifions le rĂ©sultat. Si la requĂȘte renvoie true (signifiant que la boĂźte englobante Ă©tait visible), nous rendons alors l'objet complet Ă grand nombre de polygones. Si elle renvoie false, nous pouvons sauter entiĂšrement l'appel de dessin coĂ»teux.
// --- Ătat par objet ---
const myComplexObject = {
// ... données de maillage, etc.
query: gl.createQuery(),
isQueryInProgress: false,
isVisible: true, // Supposer visible par défaut
};
// --- Boucle de rendu ---
function render() {
// ... configurer la caméra et les matrices
const object = myComplexObject;
// 1. Vérifier le résultat d'une image précédente
if (object.isQueryInProgress) {
const available = gl.getQueryParameter(object.query, gl.QUERY_RESULT_AVAILABLE);
if (available) {
const anySamplesPassed = gl.getQueryParameter(object.query, gl.QUERY_RESULT);
object.isVisible = anySamplesPassed;
object.isQueryInProgress = false;
}
}
// 2. Rendre l'objet ou son proxy de requĂȘte
if (!object.isQueryInProgress) {
// Nous avons un résultat d'une image précédente, utilisons-le maintenant.
if (object.isVisible) {
renderComplexObject(object);
}
// Et maintenant, dĂ©marrons une NOUVELLE requĂȘte pour le test de visibilitĂ© de la *prochaine* image.
// Désactiver les écritures de couleur et de profondeur pour le dessin du proxy bon marché.
gl.colorMask(false, false, false, false);
gl.depthMask(false);
gl.beginQuery(gl.ANY_SAMPLES_PASSED, object.query);
renderBoundingBox(object);
gl.endQuery(gl.ANY_SAMPLES_PASSED);
gl.colorMask(true, true, true, true);
gl.depthMask(true);
object.isQueryInProgress = true;
} else {
// La requĂȘte est en cours, nous n'avons pas encore de nouveau rĂ©sultat.
// Nous devons agir sur le *dernier* état de visibilité connu pour éviter le scintillement.
if (object.isVisible) {
renderComplexObject(object);
}
}
requestAnimationFrame(render);
}
Cette logique a un dĂ©calage d'une image, ce qui est gĂ©nĂ©ralement acceptable. La visibilitĂ© de l'objet dans l'image N est dĂ©terminĂ©e par la visibilitĂ© de sa boĂźte englobante dans l'image N-1. Cela Ă©vite de bloquer le pipeline et est significativement plus efficace que d'essayer d'obtenir le rĂ©sultat dans la mĂȘme image.
Note : WebGL2 fournit Ă©galement ANY_SAMPLES_PASSED_CONSERVATIVE, qui peut ĂȘtre moins prĂ©cis mais potentiellement plus rapide sur certains matĂ©riels. Pour la plupart des scĂ©narios d'Ă©limination, ANY_SAMPLES_PASSED est le meilleur choix.
3. RequĂȘtes de retour de transformation (`TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN`) : Compter la sortie
Le retour de transformation (Transform Feedback) est une fonctionnalité de WebGL2 qui vous permet de capturer la sortie des sommets d'un shader de sommet dans un tampon. C'est la base de nombreuses techniques GPGPU (General-Purpose GPU), comme les systÚmes de particules basés sur le GPU.
Objectif : Compter combien de primitives (points, lignes ou triangles) ont été écrites dans les tampons de retour de transformation. C'est utile lorsque votre shader de sommet peut rejeter certains sommets, et que vous avez besoin de connaßtre le compte exact pour un appel de dessin ultérieur.
Utilisation de l'API : La cible est gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
Cas d'utilisation : Simulation de particules GPU
Imaginez un systĂšme de particules oĂč un shader de sommet de type calcul met Ă jour les positions et les vitesses des particules. Certaines particules peuvent mourir (par exemple, leur durĂ©e de vie expire). Le shader peut rejeter ces particules mortes. La requĂȘte vous indique combien de particules *vivantes* restent, vous savez donc exactement combien en dessiner Ă l'Ă©tape de rendu.
// --- Dans la passe de mise Ă jour/simulation des particules ---
const tfQuery = gl.createQuery();
gl.beginQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, tfQuery);
// Utiliser le retour de transformation pour exécuter le shader de simulation
gl.beginTransformFeedback(gl.POINTS);
// ... lier les tampons et dessiner les tableaux pour mettre Ă jour les particules
gl.endTransformFeedback();
gl.endQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
// --- Dans une image ultérieure, lors du dessin des particules ---
// AprĂšs avoir confirmĂ© que le rĂ©sultat de la requĂȘte est disponible :
const livingParticlesCount = gl.getQueryParameter(tfQuery, gl.QUERY_RESULT);
if (livingParticlesCount > 0) {
// Maintenant, dessinez exactement le bon nombre de particules
gl.drawArrays(gl.POINTS, 0, livingParticlesCount);
}
Stratégie d'implémentation pratique : Un guide étape par étape
L'intĂ©gration rĂ©ussie des requĂȘtes nĂ©cessite une approche disciplinĂ©e et asynchrone. Voici un cycle de vie robuste Ă suivre.
Ătape 1 : VĂ©rification du support
Pour WebGL2, ces fonctionnalitĂ©s sont fondamentales. Vous pouvez ĂȘtre sĂ»r qu'elles existent. Si vous devez prendre en charge WebGL1, vous devrez vĂ©rifier l'extension EXT_disjoint_timer_query pour les requĂȘtes de temps et EXT_occlusion_query_boolean pour les requĂȘtes d'occlusion.
const gl = canvas.getContext('webgl2');
if (!gl) {
// Solution de repli ou message d'erreur
console.error("WebGL2 non supporté !");
}
// Pour les requĂȘtes de temps WebGL1 :
// const ext = gl.getExtension('EXT_disjoint_timer_query');
// if (!ext) { ... }
Ătape 2 : Le cycle de vie des requĂȘtes asynchrones
Formalisons le modĂšle non bloquant que nous avons utilisĂ© dans les exemples. Un pool d'objets de requĂȘte est souvent la meilleure approche pour gĂ©rer les requĂȘtes pour plusieurs tĂąches sans les recrĂ©er Ă chaque image.
- CrĂ©er : Dans votre code d'initialisation, crĂ©ez un pool d'objets de requĂȘte en utilisant
gl.createQuery(). - Commencer (Image N) : Au début du travail GPU que vous voulez mesurer, appelez
gl.beginQuery(target, query). - Ămettre les commandes GPU (Image N) : Appelez vos
gl.drawArrays(),gl.drawElements(), etc. - Terminer (Image N) : AprÚs la derniÚre commande pour le bloc mesuré, appelez
gl.endQuery(target). La requĂȘte est maintenant "en cours". - Sonder (Image N+1, N+2, ...) : Dans les images suivantes, vĂ©rifiez si le rĂ©sultat est prĂȘt en utilisant le non-bloquant
gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE). - Récupérer (Quand disponible) : Une fois que le sondage renvoie
true, vous pouvez rĂ©cupĂ©rer le rĂ©sultat en toute sĂ©curitĂ© avecgl.getQueryParameter(query, gl.QUERY_RESULT). Cet appel retournera maintenant immĂ©diatement. - Nettoyer : Lorsque vous avez dĂ©finitivement terminĂ© avec un objet de requĂȘte, libĂ©rez ses ressources avec
gl.deleteQuery(query).
Ătape 3 : Ăviter les piĂšges de performance
L'utilisation incorrecte des requĂȘtes peut nuire aux performances plus qu'elles n'aident. Gardez ces rĂšgles Ă l'esprit.
- NE JAMAIS BLOQUER LE PIPELINE : C'est la rĂšgle la plus importante. N'appelez jamais
getQueryParameter(..., gl.QUERY_RESULT)sans avoir d'abord confirmĂ© queQUERY_RESULT_AVAILABLEest vrai. Cela force le CPU Ă attendre le GPU, sĂ©rialisant ainsi leur exĂ©cution et dĂ©truisant tous les avantages de leur nature asynchrone. Votre application se figera. - FAITES ATTENTION Ă LA GRANULARITĂ DES REQUĂTES : Les requĂȘtes elles-mĂȘmes ont une petite surcharge. Il est inefficace d'envelopper chaque appel de dessin dans sa propre requĂȘte. Regroupez plutĂŽt des blocs de travail logiques. Par exemple, mesurez l'ensemble de votre "Passe d'ombres" ou "Rendu de l'interface utilisateur" comme un seul bloc, et non chaque objet projetant une ombre ou chaque Ă©lĂ©ment d'interface individuellement.
- FAITES LA MOYENNE DES RĂSULTATS DANS LE TEMPS : Un seul rĂ©sultat de requĂȘte de temps peut ĂȘtre bruitĂ©. La vitesse d'horloge du GPU peut fluctuer, ou d'autres processus sur la machine de l'utilisateur peuvent interfĂ©rer. Pour des mĂ©triques stables et fiables, collectez les rĂ©sultats sur de nombreuses images (par exemple, 60-120 images) et utilisez une moyenne mobile ou une mĂ©diane pour lisser les donnĂ©es.
Cas d'utilisation concrets et techniques avancées
Une fois que vous maßtrisez les bases, vous pouvez construire des systÚmes de performance sophistiqués.
Construire un profileur intégré à l'application
Utilisez les requĂȘtes de temps pour construire une interface de dĂ©bogage qui affiche le coĂ»t GPU de chaque passe de rendu majeure dans votre application. C'est inestimable pendant le dĂ©veloppement.
- CrĂ©ez un objet de requĂȘte pour chaque passe : `shadowQuery`, `opaqueGeometryQuery`, `transparentPassQuery`, `postProcessingQuery`.
- Dans votre boucle de rendu, enveloppez chaque passe dans son bloc `beginQuery`/`endQuery` correspondant.
- Utilisez le modĂšle non bloquant pour collecter les rĂ©sultats de toutes les requĂȘtes Ă chaque image.
- Affichez les temps en millisecondes lissés/moyennés dans une superposition sur votre canevas. Cela vous donne une vue immédiate et en temps réel de vos goulots d'étranglement de performance.
Mise à l'échelle dynamique de la qualité
Ne vous contentez pas d'un seul rĂ©glage de qualitĂ©. Utilisez les requĂȘtes de temps pour que votre application s'adapte au matĂ©riel de l'utilisateur.
- Mesurez le temps GPU total pour une image complĂšte.
- Définissez un budget de performance (par exemple, 15 ms pour laisser une marge pour un objectif de 16,6 ms/60 IPS).
- Si votre temps d'image moyen dépasse constamment le budget, baissez automatiquement la qualité. Vous pourriez réduire la résolution des cartes d'ombres, désactiver des effets de post-traitement coûteux comme le SSAO, ou baisser la résolution de rendu.
- Inversement, si le temps d'image est constamment bien en dessous du budget, vous pouvez augmenter les paramÚtres de qualité pour offrir une meilleure expérience visuelle aux utilisateurs disposant d'un matériel puissant.
Limitations et considérations sur les navigateurs
Bien que puissantes, les requĂȘtes WebGL ne sont pas sans inconvĂ©nients.
- PrĂ©cision et Ă©vĂ©nements disjoints : Comme mentionnĂ©, les requĂȘtes de temps peuvent ĂȘtre invalidĂ©es par des Ă©vĂ©nements `disjoint`. VĂ©rifiez toujours cela. De plus, pour attĂ©nuer les vulnĂ©rabilitĂ©s de sĂ©curitĂ© comme Spectre, les navigateurs peuvent intentionnellement rĂ©duire la prĂ©cision des minuteurs Ă haute rĂ©solution. Les rĂ©sultats sont excellents pour identifier les goulots d'Ă©tranglement les uns par rapport aux autres, mais peuvent ne pas ĂȘtre parfaitement prĂ©cis Ă la nanoseconde prĂšs.
- Bugs et incohérences des navigateurs : Bien que l'API WebGL2 soit standardisée, les détails d'implémentation peuvent varier entre les navigateurs et selon les différentes combinaisons OS/pilotes. Testez toujours vos outils de performance sur vos navigateurs cibles (Chrome, Firefox, Safari, Edge).
Conclusion : Mesurer pour améliorer
Le vieil adage de l'ingĂ©nierie, "on ne peut pas optimiser ce qu'on ne peut pas mesurer", est doublement vrai pour la programmation GPU. Les requĂȘtes de pipeline WebGL sont le pont essentiel entre votre JavaScript cĂŽtĂ© CPU et le monde complexe et asynchrone du GPU. Elles vous font passer de la conjecture Ă un Ă©tat de certitude Ă©clairĂ©e par les donnĂ©es sur les caractĂ©ristiques de performance de votre application.
En intĂ©grant les requĂȘtes de temps dans votre flux de travail de dĂ©veloppement, vous pouvez construire des profileurs dĂ©taillĂ©s qui localisent exactement oĂč vos cycles GPU sont dĂ©pensĂ©s. Avec les requĂȘtes d'occlusion, vous pouvez mettre en Ćuvre des systĂšmes d'Ă©limination intelligents qui rĂ©duisent considĂ©rablement la charge de rendu dans les scĂšnes complexes. En maĂźtrisant ces outils, vous gagnez le pouvoir non seulement de trouver les problĂšmes de performance, mais aussi de les corriger avec prĂ©cision.
Commencez à mesurer, commencez à optimiser, et libérez tout le potentiel de vos applications WebGL pour un public mondial sur n'importe quel appareil.